package com.softserveinc.ita.kaiji.model.game;

import com.softserveinc.ita.kaiji.exception.game.NoBotPlayerInGameException;
import com.softserveinc.ita.kaiji.exception.game.NoSuchPlayerInGameException;
import com.softserveinc.ita.kaiji.model.Card;
import com.softserveinc.ita.kaiji.model.player.Player;
import com.softserveinc.ita.kaiji.model.util.FiniteStateMachine;
import org.apache.log4j.Logger;

import java.util.*;

/**
 * Implementation of <code>Game</code> where can play one user and bot.
 * Holds only one round in one period of time.
 * @see com.softserveinc.ita.kaiji.model.game.Game
 * @author Paziy Evgeniy
 * @version 4.8
 * @since 20.03.14
 */
class PlayerVsBotGameImpl implements Game {

    private static final Logger LOGGER = Logger.getLogger(PlayerVsBotGameImpl.class);
    private static final Map<State, Set<State>> STATUS_SCHEMA = initStatusSchema();

    private Integer id;

    private volatile GameInfo gameInfo;
    private GameHistory gameHistory;

    private volatile FiniteStateMachine<State> statusChanger =
            new FiniteStateMachine<>(Game.State.GAME_INITIALIZATION, STATUS_SCHEMA);
    private volatile Round currentRound;
    private List<RoundResult> roundResultList = null;

    PlayerVsBotGameImpl(GameInfo gameInfo) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Game initialization. State=" + getState());
        }

        this.gameInfo = gameInfo;
        this.gameInfo.setGameStartTime(new Date());
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Game info: " + gameInfo);
        }

        roundResultList = new ArrayList<>();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("RoundResult list initialized: " + roundResultList);
        }

        buildHistory();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Game history initialized: " + gameHistory);
        }

        statusChanger.switchState(Game.State.GAME_STARTED);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Game initialized. State=" + getState());
        }
    }

    private void checkPlayer(Player player) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Checking player: " + player + " gameStatus=" + getState());
        }

        if (!gameInfo.getPlayers().contains(player)) {
            StringBuilder message = new StringBuilder();
            message.append("Player ");
            message.append(player);
            message.append(" not in gameInfo players: ");
            message.append(gameInfo.getPlayers());

            NoSuchPlayerInGameException ex = new NoSuchPlayerInGameException(message.toString());
            LOGGER.error(message.toString(), ex);
            throw ex;
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Checked player: " + player + ". gameStatus=" + getState());
        }
    }

    private Player findBotPlayer() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Looking for Bot player. gameStatus=" + getState());
        }

        Player bot = null;
        for (Player p : gameInfo.getPlayers()) {
            if (p.isBot()) {
                bot = p;
                break;
            }
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Chosen bot player: " + bot);
        }

        return bot;
    }

    private void makeTurnForBot(Round round) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<in> makeTurnForBot(); status=" + getState());
        }

        Player bot = findBotPlayer();

        if (bot == null) {
            String message = "There is no bot player in game marked like game with bot";
            NoBotPlayerInGameException ex = new NoBotPlayerInGameException(message);
            LOGGER.error("No bot player", ex);
            statusChanger.switchState(Game.State.GAME_BROKEN);
            throw ex;
        }

        round.makeTurn(null, bot);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<out> makeTurnForBot(); status=" + getState());
        }
    }

    private void finishRound() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<in> finishRound(); status=" + getState());
        }

        currentRound.finishRound();
        RoundResult roundResult = currentRound.getRoundResult();

        if (roundResult == null) {
            //todo exception
            throw new IllegalStateException("roundResult is null?????");
        }

        roundResultList.add(roundResult);

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("roundResult = " + roundResult);
        }

        buildHistory();

        if (!canContinueGame()) {
            finishGame();
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<out> finishRound(); status=" + getState());
        }
    }

    private boolean canContinueGame() {
        boolean result = true;
        for (Player p : gameInfo.getPlayers()) {
            if (p.getState() == Player.PlayerStatus.FINISHED) {
                result = false;
                break;
            }
        }

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Can continue game: " + result);
        }

        return result;
    }

    private void buildHistory() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<in> buildHistory(); status=" + getState());
        }

        //todo use concurrent list
        List<RoundResult> results = new ArrayList<>(roundResultList);
        results = Collections.unmodifiableList(results);
        gameHistory = new GameHistoryImpl(gameInfo, results, getState());
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Built history: " + gameHistory);
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<out> buildHistory(); status=" + getState());
        }
    }

    @Override
    public synchronized void makeTurn(Card card, Player player) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<in> makeTurn()");
        }

        statusChanger.switchState(Game.State.GAME_PLAYING);

        checkPlayer(player);

        currentRound = new StateRoundImpl();

        if (gameInfo.isBotGame()) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Game is making turn for bot...");
            }
            makeTurnForBot(currentRound);
        }

        currentRound.makeTurn(card, player);

        if (currentRound.getState() == Round.State.ROUND_READY_TO_FINISH) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Game is finishing round...");
            }
            finishRound();
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<out> makeTurn()");
        }

    }

    @Override
    public GameInfo getGameInfo() {
        return gameInfo;
    }

    @Override
    public boolean canFinishGame() {
        return statusChanger.canSwitchTo(Game.State.GAME_FINISHED)
                && currentRound.getState() == Round.State.ROUND_FINISHED;
    }

    @Override
    public synchronized State finishGame() {
        if (canFinishGame()) {
            statusChanger.switchState(Game.State.GAME_FINISHED);
            gameInfo.setGameFinishTime(new Date());
            buildHistory();
        }
        return getState();
    }

    @Override
    public boolean canInterruptGame() {
        return statusChanger.canSwitchTo(Game.State.GAME_FINISHED)
                || statusChanger.canSwitchTo(Game.State.GAME_INTERRUPTED);
    }

    @Override
    public synchronized State interruptGame() {
        if (canFinishGame()) {
            finishGame();
        } else {
            statusChanger.switchState(Game.State.GAME_INTERRUPTED);
            currentRound.interruptRound();
            gameInfo.setGameFinishTime(new Date());
            buildHistory();
        }

        return getState();
    }

    @Override
    public GameHistory getGameHistory() {
        return gameHistory;
    }

    @Override
    public final State getState() {
        return statusChanger.getCurrentState();
    }

    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public Integer getId() {
        return id;
    }

    /**
     * Making schema for Finite-state machine possible state switches
     *
     * @return schema ror Finite-state machine
     * @see com.softserveinc.ita.kaiji.model.util.FiniteStateMachine
     */
    private static Map<State, Set<State>> initStatusSchema() {
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("initialization statuses schema");
        }

        Map<State, Set<State>> statusMap = new HashMap<>();

        Set<State> gameInitialization = new HashSet<>();
        gameInitialization.add(Game.State.GAME_STARTED);
        gameInitialization.add(Game.State.GAME_BROKEN);

        Set<State> gameStarted = new HashSet<>();
        gameStarted.add(Game.State.GAME_PLAYING);
        gameStarted.add(Game.State.GAME_FINISHED);
        gameStarted.add(Game.State.GAME_BROKEN);

        Set<State> gamePlaying = new HashSet<>();
        gamePlaying.add(Game.State.GAME_PLAYING);
        gamePlaying.add(Game.State.GAME_FINISHED);
        gamePlaying.add(Game.State.GAME_INTERRUPTED);
        gamePlaying.add(Game.State.GAME_BROKEN);

        Set<State> gameFinished = new HashSet<>();

        Set<State> gameInterrupted = new HashSet<>();

        Set<State> gameBroken = new HashSet<>();

        statusMap.put(Game.State.GAME_INITIALIZATION, gameInitialization);
        statusMap.put(Game.State.GAME_STARTED, gameStarted);
        statusMap.put(Game.State.GAME_PLAYING, gamePlaying);
        statusMap.put(Game.State.GAME_FINISHED, gameFinished);
        statusMap.put(Game.State.GAME_INTERRUPTED, gameInterrupted);
        statusMap.put(Game.State.GAME_BROKEN, gameBroken);

        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("statuses schema initialized");
        }
        return statusMap;
    }

}
